﻿// <copyright file="DmeDataAccess.cs" company="City of Portland, BES-ASM">
// </copyright>
// <summary>DmeDataAccess class</summary>

namespace BESASM.EMGAATS.DataAccess
{
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Runtime.InteropServices;
  using System.Text;
  using System.Threading.Tasks;
  using BESASM.EMGAATS.Arc;
  using BESASM.EMGAATS.BusinessClasses;
  using ESRI.ArcGIS.Geodatabase;
  using ESRI.ArcGIS.Geometry;

  /// <summary>
  /// A class for reading data from the DME
  /// </summary>
  public class DmeDataAccess : INetworkReader, IArcGISDataSource
  {
    #region IArcGISDataSource Members

    /// <summary>
    /// Gets or sets the workspace representing the ArcGIS data source.
    /// </summary>
    public IWorkspace Workspace { get; set; }

    #endregion

    #region INetworkReader Members

    /// <summary>
    /// Loads the network from the workspace
    /// </summary>
    /// <param name="network">The network to load.</param>
    public void LoadNetwork(BusinessClasses.INetwork network)
    {
      if (this.Workspace == null)
      {
        throw new ArgumentNullException("workspace", "The workspace to load the network from has not been specified.");
      }

      if (network == null)
      {
        throw new ArgumentNullException("network", "The network to load has not been specified.");
      }

      this.LoadNodes(this.Workspace, network);
      this.LoadLinks(this.Workspace, network);
    }

    #endregion

    #region IEntityReader Members

    /// <summary>
    /// Gets the entity specified by querying the database.
    /// </summary>
    /// <param name="entityType">The entity type to get.</param>
    /// <param name="id">The entity id.</param>
    /// <returns>A new entity populated with data from the database.</returns>
    public IEntity GetEntity(EntityType entityType, int id)
    {
      throw new NotImplementedException();
    }

    /// <summary>
    /// Refreshes the properties of the given entity based on the data in the database.
    /// </summary>
    /// <param name="entity">The entity to be refreshed.</param>
    public void RefreshEntity(IEntity entity)
    {
      throw new NotImplementedException();
    }

    /// <summary>
    /// Returns a value indicating whether the specified entity exists in the database.
    /// </summary>
    /// <param name="entityType">The entity type to check.</param>
    /// <param name="id">The entity id.</param>
    /// <returns>A value indicating whether the specified entity exists in the database.</returns>
    public bool EntityExists(EntityType entityType, int id)
    {
      throw new NotImplementedException();
    }

    /// <summary>
    /// Returns a value indicating whether a corresponding record exists for the given entity.
    /// </summary>
    /// <param name="entity">The entity to check.</param>
    /// <returns>A value indicating whether a corresponding record exists for the given entity.</returns>
    public bool EntityExists(IEntity entity)
    {
      throw new NotImplementedException();
    }

    #endregion

    private void LoadNodes(IWorkspace workspace, BusinessClasses.INetwork network)
    {
      IFeatureWorkspace featureWorkspace = (IFeatureWorkspace)workspace;

      IFeatureClass nodeClass = featureWorkspace.OpenFeatureClass("collection_points_bes_pdx");

      int nameField = nodeClass.FindField("UNITID");

      IQueryFilter filter = new QueryFilter();
      filter.WhereClause = "SERVSTAT <> 'ABAN' AND LAYER_GROUP in ('SEWER NODES', 'ACCESS MANHOLE', 'STORM NODES')";
      IFeatureCursor cursor = nodeClass.Search(filter, false);
      try
      {
        IFeature dmeNode = cursor.NextFeature();
        try
        {
          INode node = network.CreateNode(dmeNode.OID);
          node.Geometry = new EsriPoint((ESRI.ArcGIS.Geometry.IPoint)dmeNode.ShapeCopy);
          node.NodeName = DataUtility.SafeConvertString(dmeNode.get_Value(nameField));
          node.NodeType = NodeType.Manhole;

          network.Add(node);
        }
        finally
        {
          ComUtility.ReleaseComObject(dmeNode);
        }
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }
    }

    private void LoadLinks(IWorkspace workspace, BusinessClasses.INetwork network)
    {
      Dictionary<string, List<INode>> nodesByUnitId = this._GetNodesByUnitId(network);
      Dictionary<CoordinateKey, INode> nodesByCoordinate = this._GetNodesByCoordinate(network);

      IFeatureWorkspace featureWorkspace = (IFeatureWorkspace)workspace;

      IFeatureClass linkClass = featureWorkspace.OpenFeatureClass("collection_lines_bes_pdx");

      int nameField = linkClass.FindField("GLOBALID");
      int upNodeField = linkClass.FindField("FRM_NODE");
      int downNodeField = linkClass.FindField("TO_NODE");
      int jobField = linkClass.FindField("JOBNO");
      int sizeField = linkClass.FindField("PIPESIZE");
      int lengthField = linkClass.FindField("SRVY_LEN");

      IQueryFilter filter = new QueryFilter();
      filter.WhereClause = "SERVSTAT <> 'ABAN' AND LAYER_GROUP in ('SEWER PIPES', 'STORM PIPES')";
      IFeatureCursor cursor = linkClass.Search(filter, false);
      try
      {
        IFeature dmeLink = cursor.NextFeature();
        try
        {
          ILink link = network.CreateLink(dmeLink.OID);
          ESRI.ArcGIS.Geometry.IPolyline polyline = (ESRI.ArcGIS.Geometry.IPolyline)dmeLink.ShapeCopy;
          link.Geometry = new EsriPolyline(polyline);
          link.LinkName = DataUtility.SafeConvertInt32(dmeLink.get_Value(nameField));
          link.HeightIn = DataUtility.SafeConvertDouble(dmeLink.get_Value(sizeField));

          string jobNo = DataUtility.SafeConvertString(dmeLink.get_Value(jobField));
          if (jobNo != null)
          {
            link.AsBuilts.Add(jobNo);
          }

          double length = DataUtility.SafeConvertDouble(dmeLink.get_Value(lengthField));
          if (!double.IsNaN(length))
          {
            link.LengthFt = length;
          }

          // TODO: Map materials (expand enumeration)
          link.Material = Material.Unknown;

          string upNodeName = DataUtility.SafeConvertString(dmeLink.get_Value(upNodeField));
          string downNodeName = DataUtility.SafeConvertString(dmeLink.get_Value(downNodeField));

          link.USNode = this.GetBestNodeForLink(upNodeName, polyline.FromPoint, nodesByUnitId, nodesByCoordinate);
          link.DSNode = this.GetBestNodeForLink(downNodeName, polyline.ToPoint, nodesByUnitId, nodesByCoordinate);

          network.Add(link);
        }
        finally
        {
          ComUtility.ReleaseComObject(dmeLink);
        }
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }
    }

    private INode GetBestNodeForLink(string nodeName, ESRI.ArcGIS.Geometry.IPoint point, Dictionary<string, List<INode>> nodesByUnitId, Dictionary<CoordinateKey, INode> nodesByCoordinate)
    {
      INode node = null;

      // First, check for nodes with given node name
      if (!string.IsNullOrWhiteSpace(nodeName))
      {
        if (nodesByUnitId.ContainsKey(nodeName))
        {
          List<INode> namedNodes = nodesByUnitId[nodeName];
          if (namedNodes.Count == 1)
          {
            node = namedNodes[0];
          }
          else
          {
            double minDistance = double.MaxValue;

            // If multiple nodes have same id, pick closest
            foreach (INode namedNode in namedNodes)
            {
              double dx = namedNode.X - point.X;
              double dy = namedNode.Y - point.Y;
              double distance = Math.Sqrt((dx * dx) + (dy * dy));

              if (distance < minDistance)
              {
                minDistance = distance;
                node = namedNode;
              }
            }
          }
        }
      }

      // If we can't find a node by name, check if there is one at the same location
      if (node == null)
      {
        CoordinateKey pointKey = new CoordinateKey(point.X, point.Y);
        nodesByCoordinate.TryGetValue(pointKey, out node);
      }

      return node;
    }

    private Dictionary<string, List<INode>> _GetNodesByUnitId(BusinessClasses.INetwork network)
    {
      Dictionary<string, List<INode>> nodesByUnitId = new Dictionary<string, List<INode>>(StringComparer.InvariantCultureIgnoreCase);

      foreach (INode node in network.Nodes)
      {
        if (!string.IsNullOrWhiteSpace(node.NodeName))
        {
          if (nodesByUnitId.ContainsKey(node.NodeName))
          {
            nodesByUnitId[node.NodeName].Add(node);
          }
          else
          {
            nodesByUnitId[node.NodeName] = new List<INode>() { node };
          }
        }
      }

      return nodesByUnitId;
    }

    private Dictionary<CoordinateKey, INode> _GetNodesByCoordinate(BusinessClasses.INetwork network)
    {
      Dictionary<CoordinateKey, INode> nodesByCoordinate = new Dictionary<CoordinateKey, INode>();

      foreach (INode node in network.Nodes)
      {
        CoordinateKey coordinateKey = new CoordinateKey(node.X, node.Y);
        nodesByCoordinate[coordinateKey] = node;
      }

      return nodesByCoordinate;
    }
  }
}
